package ch.ethz.inf.vs.sunspot.drivercore;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.microedition.io.Connector;
import javax.microedition.io.Datagram;

import ch.ethz.inf.vs.sunspot.Constants;
import com.sun.spot.io.j2me.radiogram.RadiogramConnection;


/**
 * This class connects to the basestation and handles 
 * the registration process of Sun SPOTs
 * 
 * @author Simon Mayer, ETH Zurich
 *
 */
public class SpotCommunicationBackend implements Runnable {
	private Thread t;

	// Logging instance
	private Logger logService = Logger.getLogger(this.getClass().getSimpleName());

	private boolean hasStarted = false;
	private boolean keepRunning = true;
	private int badConnectionCounter = 0;

	/**
	 * Create a new host instance and listen for incoming SPOT connection requests 
	 */
	public SpotCommunicationBackend() {
		logService.setLevel(Constants.LOGLEVEL);
		this.keepRunning = true;

		t = new Thread(this, "Sun SPOT Host");
		if(!this.hasStarted) t.start();
	}

	/**
	 * Stop the SPOT listener
	 */
	public void stop() {
		logService.log(Level.INFO, "Stopping SPOT Driver Communication Backend!");
		SpotManagement.getInstance().stop();
		this.keepRunning = false;
		this.hasStarted = false;
	}

	@Override
	public void run() {
		this.hasStarted = true;

		RadiogramConnection recvConn = null;
		RadiogramConnection sendConn = null;
		Datagram dg = null;
		int portBasestation = Constants.BASESTATION_PORT;

		// Establish a connection to the Sun SPOT Basestation
		boolean connected = false;
		System.setProperty("SERIAL_PORT", Constants.BASESTATION_PREFIX + portBasestation);
		logService.log(Level.INFO, "SERIAL_PORT changed to '" + System.getProperty("SERIAL_PORT") + "'");

		System.out.println("Establishing connection to Basestation at " + System.getProperty("SERIAL_PORT"));

		while (!connected && badConnectionCounter < 40) {
			try {
				recvConn = (RadiogramConnection) Connector.open("radiogram://:" + Constants.SPOT_HOST_PORT);
				recvConn.setTimeout(Constants.SPOT_HOST_TIMEOUT);
				connected = true;
				recvConn.close();
			} catch (Exception e) {
				badConnectionCounter++;
				logService.log(Level.WARNING, "Opening connection to basestation on port " + Constants.BASESTATION_PREFIX + portBasestation + " failed - retrying!");

				// Try a different port
				portBasestation = (portBasestation + 1) % 40;
				System.setProperty("SERIAL_PORT", Constants.BASESTATION_PREFIX + portBasestation);
				logService.log(Level.INFO, "SERIAL_PORT changed to '" + System.getProperty("SERIAL_PORT") + "'");
			}
		}		

		if (connected) {
			if (Constants.AUTOCONF) {
				System.out.println("Basestation found. Starting Autoconf.");

				Constants.SPOT_HOST_PORT = 100;
				int timeout = 2000;

				while (this.keepRunning) {
					try {
						recvConn = (RadiogramConnection) Connector.open("radiogram://:" + Constants.SPOT_HOST_PORT);
						recvConn.setTimeout(timeout);
						recvConn.setMaxBroadcastHops(10);
						dg = recvConn.newDatagram(recvConn.getMaximumLength());
						dg.reset();

						// Listen for incoming requests
						recvConn.receive(dg);

						recvConn.close();
					}	catch (IOException e) {
						// On timeout, close recvConn, then

						try {
							if (recvConn != null) recvConn.close();
						} catch (IOException e1) {
							logService.severe("Exception caught (lvl 1): ");
							e1.printStackTrace();
						}

						System.out.println("Nothing received on port " + Constants.SPOT_HOST_PORT);
						Constants.SPOT_HOST_PORT = (Constants.SPOT_HOST_PORT % 255) + 1;

						if (Constants.SPOT_HOST_PORT == 1) {
							timeout = timeout * 10;
							System.out.println("Increasing timeout to " + timeout + "... Restarting Autoconf.");
						}
						continue;
					}

					// Something has been received (otherwise, the continue would have kicked in...)
					break;
				}

				System.out.println("Autoconf complete. Basestation will listen on port " + Constants.SPOT_HOST_PORT);
			}
			else {
				System.out.println("Skipping Autoconf. Basestation will listen on port " + Constants.SPOT_HOST_PORT);
			}

			// Listen for spots on the same port - if one is found, negotiate the connection and connect
			while (this.keepRunning) {
				try {
					recvConn = (RadiogramConnection) Connector.open("radiogram://:" + Constants.SPOT_HOST_PORT);
					recvConn.setTimeout(Constants.SPOT_HOST_TIMEOUT);
					recvConn.setMaxBroadcastHops(10);
					dg = recvConn.newDatagram(recvConn.getMaximumLength());
					dg.reset();

					// Listen for incoming requests
					recvConn.receive(dg);

					recvConn.close();
				}	catch (IOException e) {
					// On timeout, close recvConn, then

					try {
						if (recvConn != null) recvConn.close();
					} catch (IOException e1) {
						logService.severe("Exception caught (lvl 1): ");
						e1.printStackTrace();
					}

					continue;
				}

				// Something has been received (otherwise, the continue would have kicked in...)
				try {
					if (SpotManagement.getInstance().isActive) {
						// Read sender's ID
						String spotAddress = dg.getAddress();  
						String spotName = dg.readUTF();

						// Log it
						logService.log(Level.FINE, "Host received something: " + spotName);

						// Establish a channel back to the SPOT
						sendConn = (RadiogramConnection) Connector.open("radiogram://" + spotAddress + ":" + Constants.SPOT_HOST_PORT);
						sendConn.setMaxBroadcastHops(0);
						dg = sendConn.newDatagram(sendConn.getMaximumLength());
						dg.reset();

						// If the SPOT isn't blocked, welcome it and initialize and create a new SPOT representation + synchronizer thread instance for it
						if (!SpotManagement.getInstance().getBlockedSpots().contains(spotName)) {
							// Create management structures of new SPOT, Management assigns the spot's port		
							int spotPort = SpotManagement.getInstance().createNewSpot(spotName, spotAddress);

							String xmitString = SpotRepresentation.MESSAGE_SPOT_HELO + SpotRepresentation.SEPARATOR + Constants.SPOT_SYNC_INTERVAL + SpotRepresentation.SEPARATOR + spotPort;
							System.out.println("--[join]--> Sun SPOT " + spotName);

							dg.writeUTF(xmitString);						
						}
						else {
							// Tell the SPOT that it is blocked
							String xmitString = SpotRepresentation.YOU_ARE_BLOCKED;
							System.out.println("-[block]--| Sun SPOT " + spotName);
							dg.writeUTF(xmitString);
						}

						sendConn.send(dg);
						sendConn.close();
					}
				}  catch (Exception e) {
					if (e.getMessage().contains("NoRouteException")) {
						System.out.println("No route found to SPOT!");
					}
					else {
						logService.info("Exception caught: " + e.getMessage());
						e.printStackTrace();
					}

					try {
						if (sendConn != null) sendConn.close();
					} catch (IOException e1) {
						logService.severe("Exception caught (lvl 1): ");
						e1.printStackTrace();
					}
				}
			}

			logService.log(Level.INFO, "STOPPED SpotDriverHost!");
		}
		else {
			logService.log(Level.SEVERE, "Unable to connect to Basestation!");
		}
	}
}
